如何创建EasyAR头显扩展

这篇文章将说明如何在一个尚未受到EasyAR支持的头显设备上支持EasyAR的功能。EasyAR已经支持的头显及常规使用说明请参考 EasyAR头显扩展

在该文档成文之时(2023年),AR/VR/MR/XR行业内还没有形成非常统一的接口方案,虽然OpenXR是个很好的候选,但规范演化和行业实现还需要时间。所以通常来说市贩设备直接运行EasyAR并不那么容易,很有可能存在数据接口缺失的情况(大概率)。如果你是应用或内容开发者,请联系硬件制造方或EasyAR商务。

因此这份文档主要的阅读者是硬件供应商,而非普通消费者。通常来说,本文档在提供规范的同时并不限定所有实现细节,任何实现方式或接口定义都可以讨论,欢迎通过商务渠道联系沟通。

本文档覆盖的硬件自身需要有运动跟踪(VIO)能力。通常我们假定EasyAR的功能运行在良好的设备跟踪能力之上,通常不建议靠EasyAR优化设备的跟踪从而产生循环依赖(站在最上层架构上考虑,不排除误差被正反馈放大从而导致系统趋于不稳)。如果设备本身没有运动跟踪能力,那么支持方案并不在本文档覆盖范围之内,如有需要可通过商务渠道进行沟通。

目标与合理预期

不要误解即将完成的目标是极其重要的。对即将投入的资源以及最终会获得的结果要有合理预期。

什么是EasyAR头显扩展

你在写的扩展是,

  • 使用 EasyAR Sense 的自定义相机接口,从你的设备API抓取数据并发送进 EasyAR Sense 的一堆代码。

  • 在Unity中,头显扩展会使用可继承的frame source API和 EasyAR Sense Unity Plugin 定义的一套 EasyAR Sense 数据流来简化 EasyAR Sense 自定义相机开发。

  • 在Unity中,头显扩展是一个 Unity package,包含运行时脚本,编辑器脚本和扩展的sample,你或EasyAR可以将它分发给下游用户。

你在写扩展的时候,可能会,

  • 修改你的SDK接口设计和内部实现。

  • 与你的团队一起讨论确认数据获取和使用方案。

  • 花大量时间进行数据正确性验证而不是写代码。

写完扩展后,你将会有,

  • 大多数 EasyAR Sense 功能(比如图像跟踪,稠密空间地图)在你的设备上可以使用,这些功能会利用你设备的运动跟踪(VIO)能力。

  • EasyAR Sense 内支持的EasyAR云服务在你的设备上可以使用。

  • 使用自定义相机时的所有 EasyAR license 限制以相同方式适用于你的设备。

什么不是EasyAR头显扩展

这个扩展不能脱离 EasyAR Sense 使用,

  • 这个头显扩展不会独立运行,作为依赖, EasyAR Sense 也是需要的。在Unity中则必须使用 EasyAR Sense Unity Plugin

  • 它不会直接调用EasyAR云服务API,比如EasyAR Mega定位服务,它们在 EasyAR Sense 中完成。

  • 在Unity中,不会直接调用比如图像跟踪API的方法,它们在 EasyAR Sense Unity Plugin 中完成。

  • 在Unity中,头显扩展不会修改场景中物体或跟踪目标的 transform,它们在 EasyAR Sense Unity Plugin 中完成。

这个扩展不能脱离你的设备SDK使用,

  • 在Unity中,头显扩展或 EasyAR Sense Unity Plugin 不会修改场景中相机的 transform,这必须在你的设备SDK中完成。

通过头显扩展有一些EasyAR功能仍是无法使用的,

  • 表面跟踪功能将无法使用。

  • EasyAR运动跟踪将无法使用。

  • 平面检测(EasyAR运动跟踪的一部分)将无法使用。

通常来说,写扩展不能将资源限制在使用Unity做开发上面,

  • 由于缺少标准,通常它无法只在3D引擎上面实现,所以建议从第一天起就让系统工程师和SDK工程师等底层开发工程师参与进来。

所以我该如何在我的设备上使用Mega呢?

在你的设备上运行验证 EasyAR Sense ,然后EasyAR Mega将会自然被支持,不需要其它多余工作。请确保不要一开始就直接使用Mega sample来在你设备上运行验证EasyAR,你很可能会失败。

背景知识

打造 AR/VR 设备需要一些领域知识,相似地,在设备上运行验证 EasyAR Sense 将需要你或你的团队是如下领域的专家,

如果你工作在Unity上,你还需要知道这些,

另外,有一点在这些领域的知识将帮助你更好地理解系统,尤其是如何发送正确的数据到EasyAR,

数据需求

为使EasyAR在你的设备上工作,最重要的工作同时也是最棘手的部分是确保数据正确性。

EasyAR Sense 通常需要两组数据,我们根据接口调用时间和数据特征,将这两组数据称为,

  1. 图像帧数据(Image frame data)

  2. 渲染帧数据(Rendering frame data)

图像帧数据

数据需求:

  1. 时间戳(Timestamp)

  2. 跟踪状态(Tracking status)

  3. 相机位姿(Camera pose)

  4. 内参(Intrinsics,包括图像大小、焦距、主点。如果有畸变还需要畸变模型和畸变参数)

  5. 原始相机图像数据

数据时间:

  • 曝光中点

数据使用:

  • API调用时间:可根据你的设计改变。一个大多数设备使用的常规方法是在3D引擎的渲染更新中查询,然后根据frame中的时间戳来判断是否进一步进行数据处理

  • API调用线程:3D引擎的game thread或任何其它线程(如果你的所有API都是线程安全的)

Unity中API调用示例如下,

void TryInputCameraFrameData()
{
    double timestamp;

    if (timestamp == curTimestamp) { return; }
    curTimestamp = timestamp;

    PixelFormat format;
    Vector2Int size;
    Vector2Int pixelSize;
    int bufferSize;

    var bufferO = TryAcquireBuffer(bufferSize);
    if (bufferO.OnNone) { return; }
    var buffer = bufferO.Value;

    IntPtr imageData;
    buffer.tryCopyFrom(imageData, 0, 0, bufferSize);

    Pose cameraPose;
    MotionTrackingStatus trackingStatus;

    using (buffer)
    using (var image = Image.create(buffer, format, size.x, size.y, pixelSize.x, pixelSize.y))
    using (var frame = InputFrame.create(image, cameraParameters, timestamp, cameraPose.ToEasyARPose(), trackingStatus))
    {
        sink.handle(frame);
        ReceivedFrameCount++;
    }
}

上面的代码不会通过编译,它只是一个在Unity中的简化的API调用示例。请通过 com.easyar.sense.ext.hmdtemplate 模板获取可以使用的代码样例。

渲染帧数据

数据需求:

  1. 时间戳(Timestamp)

  2. 跟踪状态(Tracking status)

  3. 相机位姿(Camera pose)

数据时间:

  • 上屏时刻。Timewrap不计算在内。相同时刻的head pose数据会被用来设置3D相机的transform以渲染当前帧。

数据使用:

  • API调用时间:3D引擎的每个渲染帧

  • API调用线程:3D引擎的game thread

Unity中API调用示例如下,

void InputRenderingFrameData()
{
    double timestamp;
    Pose cameraPose;
    MotionTrackingStatus trackingStatus;

    UpdateMotion(timestamp, cameraPose, trackingStatus);
}

上面的代码不会通过编译,它只是一个在Unity中的简化的API调用示例。请通过 com.easyar.sense.ext.hmdtemplate 模板获取可以使用的代码样例。

额外细节

相机图像数据,

  • 图像坐标系:在传感器水平时获取的数据也应是水平的。数据应该以左上角为原点,行优先存储。图像不应翻转或颠倒。

  • 图像FPS:正常 30或60 fps 的数据都可以。如果高fps有特殊影响,为达到合理的算法效果,最小可接受帧率为2。建议使用高于2的fps,通常情况下使用原始数据帧率即可。

  • 图像大小:为获取更好的计算结果,最大边应为960或更大。正常不鼓励在数据链路中进行耗时的图像缩放,建议直接使用原始数据,除非完整大小的数据拷贝时间已经长得无法接受。

  • 像素格式:优先跟踪效果并综合考虑性能的话,通常格式优先顺序为 YUV > RGB > RGBA > Gray (YUV中的Y分量)。在使用YUV数据时,需要完整的数据定义,包括数据封装和填充细节。相较单通道图像而言,使用彩色图像EasyAR Mega的效果会更好,但其它功能影响不大。

  • 数据访问:数据指针或等价实现。最好在数据链路中消除所有可能的非必须拷贝。EasyAR会在数据复制后异步使用。注意数据所有权。

时间戳,

  • 所有时间戳都应时钟同步,最好是硬件同步。

跟踪状态,

  • 跟踪状态由设备定义,需要包含跟踪丢失(VIO不可用)的状态。如有更多等级则更好。

相机位姿,

  • 主要有两种实现,

    1. 直接获取 camera pose。

    2. 获取 head pose,以及一个标定矩阵 T_head_(rgb)camera,用于从head坐标系变换到 (RGB) camera 坐标系。

  • 所有pose(包括3D引擎中的相机transform)都应使用同一个原点。

  • 在Unity中,pose数据应以Unity坐标系定义方式给出。如果头显扩展由EasyAR实现且pose使用了其它坐标系定义方式,你应提供清晰的坐标系定义或给出转换到Unity坐标系的方法。

  • 在Unity中,如果使用Unity XR 框架,只需要兼容 device模式即可。

内参,

  • 所有数值都应与图像数据匹配。如有需要请在发送给EasyAR之前对内参进行缩放。

  • 如果头显扩展由EasyAR实现,你应说明内参是否会在每一帧变化(区别是对应API应该调用一次还是每帧调用)。

性能,

  • 数据应以最优效率提供。在大多数实现中,API调用会发生在渲染过程,所以建议即使在底层需要进行耗时操作的情况下,也不要阻塞API调用,或者以合理的方式来使用这些API。

  • 如果头显扩展由EasyAR实现,你需要对所有耗时API调用进行说明。

多相机,

  • 至少一个相机的数据是需要的。这个相机可以是RGB相机、VST相机、定位相机等中的任意一个。如果只有一个相机,我们通常推荐使用在中央或在眼睛附近的RGB相机或VST相机。

  • 使用多相机可提升EasyAR算法效果。所有可用相机的 Image frame data 的数据应在同一个时间点同时提供。

多相机目前尚未完全支持,请联系我们获取更多细节。

准备工作

有一些工作需要在开始写头显扩展之前完成。

为AR/MR准备你的设备

  • 准备你的 SLAM/VIO 系统

    确保设备跟踪误差受控。一些 EasyAR 功能比如Mega可以在某种程度上降低设备累积误差,但大的局部误差也会让EasyAR的算法变得不稳定。通常来说,我们期望VIO漂移小于1‰。有意图地使用EasyAR来降低VIO误差是错误的。

  • 准备你的显示系统

    确保当一个与现实中某个物体大小和轮廓相同的虚拟物体被放置在虚拟世界中,且它与相机的相对变换关系与真实世界中对应物体与设备的变换关系相同,在这样的情况下,虚拟物体可以覆盖显示在真实物体上,且移动设备不会打破显示效果。

  • 准备你的设备SDK

    确保你已经有API可以提供 数据需求 段落中所描述的数据需求。正常情况下使用一个或两个API来获取这两组数据会是比较好的,因为这些数据应该由你系统中的两个且只有两个时间点产生。如果API过多会容易出现设计上考虑不充分从而导致数据无法对齐的情况。

从应用开发角度学习 EasyAR Sense

先学习一下 EasyAR Sense 是很重要的。如果你在使用Unity,则还需要学习 EasyAR Sense Unity Plugin 。你必须能够在Android系统下运行样例,EasyAR需要一些基础配置,同时你也会学到如何使用 EasyAR Sense 的许可证。

建议先在手机上运行Unity中的这些样例,学习这些功能的行为是什么样的。在你的设备上运行这些功能的时候并不会有根本性的不同。

注意:即使你工作在其它3D引擎上,也请确保运行一下Unity样例。

为开发Unity的package做好准备

你可以通过Unity的 Package Manager window使用本地tarball文件安装插件 导入 EasyAR Sense Unity Plugin (package com.easyar.sense),或者也可以使用任何Unity允许的方式来导入它。

你应该解压头显扩展模板 (package com.easyar.sense.ext.hmdtemplate)到你可以开发package的位置,或者根据 Unity创建自定义package的指南 来创建一个新的package。

注意:如果你的设备SDK未使用Unity的package来组织,你需要解压头显扩展模板到Unity的Assets文件夹,然后从解压的文件中删除package.json以及任何以 .asmdef 为后缀名的文件。请注意在这种使用方式下,同时使用两个SDK的用户将无法获得合理的版本依赖。

熟悉头显模板

com.easyar.sense.ext.hmdtemplate package 是为你准备的样例以及模板。它是一个SDK的实现,并且包含了给你用户的样例。你应该先熟悉一下所有文件。在你知道那些文件尤其是所有脚本文件的用途之前不要急于做修改,你应该完全掌控这个package。

这个头显扩展是一个SDK,它的上游是 EasyAR Sense (在Unity中是 EasyAR Sense Unity Plugin )以及你的设备SDK,下游是用户app。为了开发一个SDK,你不仅需要从app开发的视角看这个package,还需要以SDK供应商的视角来思考。因此,你需要比正常app开发者学习更多的EasyAR细节。

这个package的包结构遵循了 Unity推荐的文件布局

.
├── CHANGELOG.md
├── Documentation~
├── Editor
├── LICENSE.md
├── package.json
├── Runtime
└── Samples~
    └── SimpleDemo

请确保通过 Unity 文档 来学习package开发。我们在此列出一些要点如下,

  • Runtime :存放运行时平台资产的文件夹。这是模板中最重要的文件夹 ,你将主要修改它里面的脚本。

  • Samples~ :存放package中所有样例的文件夹。它包含给下游使用的样例,你可以将它用作测试扩展的demo。为了原地开发这个样例,请确保修改文件夹名为 Samples 。Unity的 Client.Pack 方法会在你打包一个新的发布时将其自动重命名为 Samples~

  • Editor :存放编辑时平台资产的文件夹。这个文件夹的脚本主要用于创建菜单项,通常你需要修改其中的一部分文字用以代表你的设备。

  • package.json :package的清单文件,请确保在发布前修改。

深入了解 EasyAR Sense Unity PluginARSession 工作流

../_images/image_h2_1.png

在使用头显时,相机的transform不会被修改。

请阅读 ARSession 的API文档或源码来学习更多细节。

深入了解 EasyAR Sense Unity Plugin : Frame Source

写头显扩展时最重要的部分就是写一个自定义相机设备(或者在Unity插件中,写一个frame source)。

Frame source 是在 EasyAR Sense Unity Plugin 中设计的一个组件,用于抽象所有相机设备以及其它提供图像(和pose)的组件。在 EasyAR Sense Unity Plugin 中, CameraDeviceFrameSourceMotionTrackerFrameSourceARKitFrameSourceARCoreFrameSource 都表达了frame source。而在 EasyAR Sense 中,同功能的组件为 CameraDeviceMotionTrackerCameraDeviceARKitCameraDeviceARCoreCameraDevice 。请确保阅读 EasyAR SenseAPI 概览 来学习 EasyAR Sense 数据流和自定义相机。

在Unity中,你可以在创建你的头显扩展时使用一些预定义的抽象 frame source作为基类。

../_images/image_h2_2.png

上图显示了这些frame source的相互关系,以及EasyAR提供的一些头显设备支持的frame source。

在写一个新的头显扩展的时候,你需要知道一些重要的细节。

FrameSource:

可用性(Availability)

所有EasyAR功能(包括设备支持)都会定义可用性接口,以便让用户知道对应功能在运行时在session的某个状态下以及在特定设备上是否可以使用。可用性接口也会在 ARSession 启动时使用,不可用的组件将不会被选择且它的方法在 ARSession 运行(running)时不会被调用。

  • 属性 IsAvailable :frame source是否可用。你应该根据frame source的内部状态定义你自己的数值。如果这个值等于null, ARComponentPicker 会在选择frame source的时候调用 CheckAvailability

  • 方法 CheckAvailability :进行可用性检查(换句话说,检查设备是否可被支持以及场景里的物体是否已经准备好)。在这个方法结束前其它流程不会开始。如果 IsAvailable == false,则这个frame source不会被选择。

渲染相机(Rendering Camera)

frame source中定义的渲染相机会用来在你的眼前显示 ARSession 运行时的一些信息。每个frame source都有自己的方法来检查相机是否可用,如果设置的相机不可用,frame source将会拒绝设置。

  • 属性 Camera :渲染相机。这个属性会在 ARComponentPicker 选择frame source时使用,会用来判断frame source是否可用。应用开发者可能会在session启动前设置它为一个可用的 UnityEngine.Camera。

  • 方法 PickCamera :在 ARComponentPicker 选择组件时,如果 Camera 是null, ARComponentPicker 会用它选择和设置 Camera

  • 方法 IsValidCamera :它被用于决定设置给 Camera 的相机是否有效。它将在用户从Unity inspector设置相机或由 PickCamera 自动选择相机时被调用。

会话原点(Session Origin)

session origin用于设置 SessionOrigin 中心模式下的 transform 基准点,并用于在其它中心模式下同时变换 camera-origin 对。如果你的SDK是基于Untiy XR框架进行设计的,你会对 XR.CoreUtils.XROrigin 比较熟悉。Origin就是类似 XR.CoreUtils.XROrigin 的东西,渲染相机是它的儿子节点。用户可以在场景中移动 origin ,而不损失由 camera-origin对定义的局部运动关系。EasyAR会使用camera-origin对来处理target中心模式,这在运行Mega时尤其有用,因为在Mega中定义了整个地球的现实世界坐标系,而 camera-origin对通常定义的是相对于某个启动点的VIO坐标系。

  • 属性 Origin :在 ARSession 中使用的 session origin。

会话启动(Session Startup)

  • 方法 OnAssemble :它会在 ARSession 进行组装(assembling)的时候调用。这只会在 ARComponentPicker 结束选择组件且你的frame source已经被选择之后才会发生。这个方法设计上是用来做延迟初始化的,你可以在这个方法中做AR独有的初始化工作。需要确保先调用base.OnAssemble。

额外控制(Extra Controls)

  • 属性 IsCameraUnderControl :在使用头显时,IsCameraUnderControl应被设为false(正常会在 ExternalDeviceMotionFrameSource 中处理),这样整个插件会,

    1. 不会设置相机的transform。你应该完全控制场景中的3D相机。

    2. 不会渲染相机图像。你应该处理相机渲染,尤其是在VST模式下。

  • 属性 IsHMD :在使用头显时,IsHMD应被设为true,这样整个插件会,

    1. 使用 Camera 前的一个3D板子来显示弹出信息(手机上会使用2D UI显示)。确保在frame source中设置正确的 Camera

    2. 在Mega中不使用加速度计输入。正常情况下你不需要关心这个不同带来的影响。

ExternalDeviceMotionFrameSource:

“External”设备运动的意思是,SLAM由非EasyAR组件提供,并且3D相机的transform已经由你的设备SDK控制。

  • Override IsCameraUnderControl :false

  • 定义 buffer pool :BufferCapacity会被处理

  • 定义 ReceivedFrameCount :插件会用它来检查设备帧输入的健康情况

  • 定义 UpdateMotion 方法 :运动更新会在 ARSession 中处理

WorldOriginBasedFrameSource:

  • Override 可用 Center Mode :SessionOrigin

  • Override Origin :会创建WorldRoot并被用来作为Origin

RelativeOriginBasedFrameSource:

  • Override 可用 Center Mode :SessionOrigin, FirstTarget, SpecificTarget

  • Override Origin :由你的设备SDK定义

XROriginBasedFrameSource:

CompatibleVROriginFrameSource:

  • Override Origin :兼容 XR.Interaction.Toolkit.XRRig 和 XR.CoreUtils.XROrigin

  • Override IsHMD :true

  • Override OnAssemble :定义InitializeCamera并在OnAssemble中被调用

请阅读每个frame source的API文档或源代码来学习更多细节。

如果你需要定义Unity消息,比如Awake或OnEnable,请确保检查你的基类是否已经使用这些方法,并在你的实现中调用基类中的方法。

EasyAR Sense Unity Plugin 写头显扩展

在这个段落中,我们会使用package com.easyar.sense.ext.hmdtemplate 来演示,但可能不会覆盖模板中的所有细节。在写你的头显扩展时,请确保阅读package中的所有细节。源码中已经有所有接口和数据需求的解释。

注意:不同版本的实现细节可能会有所区别。

写头显扩展:基类选择

我们已知在头显设备上运行Unity有很多不同的实现方式。你需要知道你使用的是哪一种,然后根据你的实现细节来选择继承的frame source基类。虽然你永远可以继承 ExternalDeviceMotionFrameSourceFrameSource ,但这也意味着比起继承其它基类,你需要做更多的工作。

典型地,我们推荐使用下面顺序的设计来实现你的SDK框架,

  1. 使用Unity XR框架(支持 XR Interaction ToolkitAR Foundation 或全支持)

  2. 设计一个类似于 XR.CoreUtils.XROrigin 的 session origin

  3. 所有其它

关于 AR Foundation 的补充: AR Foundation 目前并没有准备好用于EasyAR这样的使用方式。有一些像Snapdragon Spaces这样的SDK会使用AR Foundation来提供图像数据,但它们也同时会通过它们自己的接口来提供额外的数据。我们不推荐在当前阶段使用AR Foundation接口,因为它的接口设计有一些限制,尤其是多年缺少image pose这样的数据。当然,我们鼓励与Unity协作以改善将来的API设计。

基于你的实现,你可用选择继承,

  1. CompatibleVROriginFrameSource 如果你的设备SDK是基于 XR Interaction ToolkitAR Foundation 的。

    CompatibleVROriginFrameSource 设计用于提供 XR Interaction Toolkit 的兼容性,这对一些VR头戴设备尤其重要,它们可能比 XR Interaction Toolkit 还要更早存在。如果你不需要这样的兼容性,你可用使用 XROriginBasedFrameSource ,但请注意参考 CompatibleVROriginFrameSource 源码来保持一些针对头显设备的特殊的调整。

  2. XROriginBasedFrameSource 如果你的设备SDK是基于类似 AR Foundation 的Unity XR框架但没有使用XR Interaction Toolkit。

    请确保参考 CompatibleVROriginFrameSource 源码来保持一些针对头显设备的特殊的调整。

  3. RelativeOriginBasedFrameSource 如果你的设备SDK没有使用Unity XR框架,但有类似 XR.CoreUtils.XROrigin 的设计。

    你可以通过这个类获得更多灵活性。

  4. WorldOriginBasedFrameSource 如果你的设备SDK没有定义类似 XR.CoreUtils.XROrigin 的原点。

    你会损失一些灵活性,尤其是只能支持有限的中心模式,物体的移动方式也会随之受限。应用开发者必须对于他们如何摆放虚拟物体十分小心,因为在使用这个类的时候EasyAR节点和物体永远都会动。所有放在Unity世界坐标系下的物体在任何配置下都永远不可能显示在正确的位置。

在进入下一个段落之前,让我们看一下模板样例中是如何提供这些类型的frame source的。模板只定义了一个frame source,但是使用了分部类。这一个frame source有3种不同的实现,它们通过定义在EasyAR.Sense.Ext.HMDTemplate.asmdef文件中的宏定义来分隔。你可以修改 .asmdef 文件中的宏定义来快速获取支持你的设备的frame source应该是什么样。这些宏和分部类并不是以功能演示或代码参考的目的提供的,所以不要将它们当作黑盒,请以适合你的方法去修改和使用。如果你发现最后完成开发之后只修改了一个脚本文件,那你不会获得一个可以正常使用的frame source。

基于你的设备SDK的实现方式,这里提供更多关于这些实现的说明如下:

  1. 使用Unity XR框架以及 XR Interaction Toolkit

    你应该打开宏 MY_SDK_IS_USING_XRInteractionToolkit ,然后 CompatibleVROriginFrameSource 会被继承。 XR Interaction Toolkit 定义origin为 XR.CoreUtils.XROrigin (在老版本中是XRRig), CompatibleVROriginFrameSource 会利用这个原点。如果你发现一个不被支持的XR Interaction Toolkit版本,你也可以自己实现对应功能。

  2. 使用Unity XR框架但未使用 XR Interaction Toolkit

    你应该打开宏 MY_SDK_IS_USING_XRInteractionToolkit ,修改代码以继承 XROriginBasedFrameSource ,然后参考 CompatibleVROriginFrameSource 代码来调整对应代码。

  3. 设计了类似 XR.CoreUtils.XROrigin 的原点但未使用Unity XR框架

    你应该打开宏 MY_SDK_DESIGNED_ORIGIN ,然后 RelativeOriginBasedFrameSource 会被继承。你的设备SDK定义的原点会被使用。

  4. 没有设计Session origin

    你应该打开宏 MY_SDK_DOES_NOT_DESIGN_ORIGIN ,然后 WorldOriginBasedFrameSource 会被继承。 WorldOriginBasedFrameSource 会自动生成一个带有 WorldRootController 的物体来代表原点。 WorldOriginBasedFrameSource 永远不会移动生成出来的原点,因此只有SessionOrigin中心模式可以被支持。如果你设计上只关心相机且不希望用户在不改变现实世界中物体的相对关系的前提下在Unity世界坐标系下自由地操作他们的物体,那这样的情况是有用的。

写头显扩展:可用性

所有EasyAR功能(包括设备支持)都会定义可用性接口,以便让用户知道对应功能在运行时在session的某个状态下以及在特定设备上是否可以使用。可用性接口也会在 ARSession 启动时使用,不可用的组件将不会被选择且它的方法在 ARSession 运行(running)时不会被调用。

你需要override属性 IsAvailable 和方法 CheckAvailability 。如果 IsAvailableARComponentPicker 挑选frame source之前有值,则 CheckAvailability 不会被调用。 CheckAvailability 是一个协程。有时在可用性可被决定之前你可能需要等待设备准备好或等待数据更新。如果不需要等待可以直接返回null。

public override Optional<bool> IsAvailable { get { return isAvailable; } }

public override IEnumerator CheckAvailability()
{
    yield return new WaitUntil(() => false); // NOTE: Wait until you can determine availability, so don't forget to change this endless waiting sample.

    isAvailable = true; // NOTE: Make sure to let IsAvailable has value and set to true only when device can be supported.

    throw new NotImplementedException("Please finish this method using your device SDK API");
}

在继承 CompatibleVROriginFrameSourceXROriginBasedFrameSource 时你还需要检查原点是否在场景中存在。

public override IEnumerator CheckAvailability()
{
    ...
    if (isAvailable.OnSome && isAvailable.Value)
    {
        isAvailable = CheckOrigin(true);
    }
    ...
}

举例, NrealFrameSource 中的实现方式如下,

public override Optional<bool> IsAvailable { get { return isAvailable; } }

public override IEnumerator CheckAvailability()
{
    SetupCameraRig();
    isAvailable = cameraRig && cameraRig.gameObject.activeInHierarchy;
    return null;
}

写头显扩展:渲染相机

frame source中定义的渲染相机会用来在你的眼前显示 ARSession 运行时的一些信息。每个frame source都有自己的方法来检查相机是否可用,如果设置的相机不可用,frame source将会拒绝设置。

你需要override方法 PickCameraIsValidCamera 。如果 CameraARComponentPicker 挑选frame source之前有值,则 PickCamera 不会被调用。 IsValidCamera 会在 ARComponentPickerPickCamera 中挑选相机或由app开发者设置相机时被用来判断一个相机是否一个有效的渲染相机。

public override Camera PickCamera()
{
    throw new NotImplementedException("Please finish this method using your device SDK API");
}

protected override bool IsValidCamera(Camera cam)
{
    throw new NotImplementedException("Please finish this method using your device SDK API");
}

在继承 CompatibleVROriginFrameSourceXROriginBasedFrameSource 的时候不需要提供渲染相机,这些类会直接使用Unity XR框架中定义的相机。

举例, NrealFrameSource 中的实现方式如下,

public override Camera PickCamera()
{
    if (!cameraRig) { return null; }
    return CameraRig.centerCamera;
}

protected override bool IsValidCamera(Camera cam)
{
    var rig = CameraRig;
    if (!rig) { rig = FindObjectOfType<NRHMDPoseTracker>(); }
    if (!rig) { return false; }
    return cam == rig.centerCamera;
}

写头显扩展:Session原点

session origin用于设置 SessionOrigin 中心模式下的 transform 基准点,并用于在其它中心模式下同时变换 camera-origin 对。如果你的SDK是基于Untiy XR框架进行设计的,你会对 XR.CoreUtils.XROrigin 比较熟悉。Origin就是类似 XR.CoreUtils.XROrigin 的东西,渲染相机是它的儿子节点。用户可以在场景中移动 origin ,而不损失由 camera-origin对定义的局部运动关系。EasyAR会使用camera-origin对来处理target中心模式,这在运行Mega时尤其有用,因为在Mega中定义了整个地球的现实世界坐标系,而 camera-origin对通常定义的是相对于某个启动点的VIO坐标系。

当继承 RelativeOriginBasedFrameSource 时,你需要override属性 Origin 来返回你的设备SDK定义的原点。

public override GameObject Origin
{
    get
    {
        throw new NotImplementedException("Please finish this method using your device SDK API");
    }
}

CompatibleVROriginFrameSourceXROriginBasedFrameSource 会设置 OriginXR.CoreUtils.XROrigin 或其它Unity定义的原点,你不需要override这个属性。

WorldOriginBasedFrameSource 会使用 WorldRootController 创建 Origin ,你不需要override这个属性。

写头显扩展:额外控制

Override IsHMD 并设为true。

public override bool IsHMD { get => true; }

IsCameraUnderControl 会由 ExternalDeviceMotionFrameSource 设为true,所以正常你不需要override它。但如果你直接继承 FrameSource ,则必须 override 这个属性并设为true。

写头显扩展:Session启动

OnAssemble 会在 ARSession 进行组装(assembling)的时候在每个EasyAR组件上调用。 该frame source的 OnAssemble 会在 ARComponentPicker 结束选择组件且你的frame source已经被选择之后才会发生。这个方法设计上是用来做延迟初始化的。

你需要override方法 OnAssemble 然后做AR独有的初始化工作。需要确保先调用base.OnAssemble。

public override void OnAssemble(ARSession session)
{
    base.OnAssemble(session);
    StartCoroutine(InitializeCamera());
}

继承 CompatibleVROriginFrameSourceXROriginBasedFrameSource 时不需要 override OnAssemble ,它们会提供InitializeCamera来作为替代。

这里是打开设备相机(比如RGB相机或VST相机等)的好地方,尤其是如果这些相机没有被设计成要一直打开时。同时这里也是获取整个生命周期内不会变化的标定数据的好地方。有时在这些数据可以被获取前你可能需要等待设备准备好或等待数据更新。

private IEnumerator InitializeCamera()
{
    yield return new WaitUntil(() => false); // NOTE: Wait until device initialized, so don't forget to change this endless waiting sample.

    ...
    cameraParameters = CameraParameters.createWithCustomIntrinsics(imageSize, focalLength, principalPoint, distCoeffs, cameraModel, cameraDeviceType, cameraOrientation); // NOTE: If online optimization exists, generate in TryInputCameraFrameData instead.

    CalibratedCameraOffset = new Pose(); // NOTE: Replace with device calibration data. This value may not be necessary depending on your API, see where it is used.

    StartCoroutine(InputDeviceData()); // NOTE: Start to input data into EasyAR.

    throw new NotImplementedException("Please finish this method using your device SDK API");
}

同时,这里也是一个启动数据输入循环的好地方。你也可以在 Unity 脚本 Update 或其它方法中写这个循环,尤其是当你的数据需要在Unity执行顺序的某个特殊时间点获取的时候。在session准备好(ready)之前不要输入数据。

private IEnumerator InputDeviceData()
{
    yield return new WaitUntil(() => arSession && arSession.State >= ARSession.SessionState.Ready);

    while (true)
    {
        InputRenderFrameMotionData();
        TryInputCameraFrameData();
        yield return null;
    }
}

如果你希望,你也可以忽略启动过程并在每次更新时做数据检查,这完全取决于你。

举例, QiyuFrameSource 中的实现方式如下,

protected override IEnumerator InitializeCamera()
{
    QiyuARCore.InitAR();
    if (ControlSeeThrough)
    {
        QiyuARCore.EnableSeeThrough(true);
    }

    var frameData = new QiyuARPlugin.VstFrameDataNative();
    QiyuARPlugin.QVR_GetVstFrame(ref frameData);
    while (frameData.headTimeStamp <= 0 || frameData.dataLength <= 0)
    {
        QiyuARPlugin.QVR_GetVstFrame(ref frameData);
        yield return null;
    }

    QiyuARPlugin.QVR_GetVstFrame(ref frameData);
    var radialDisortion = new float[4];
    System.Runtime.InteropServices.Marshal.Copy(frameData.radialDisortion, radialDisortion, 0, radialDisortion.Length);
    cameraParameters = CameraParameters.createWithCustomIntrinsics(new Vec2I((int)frameData.size.x, (int)frameData.size.y), new Vec2F(frameData.focal.x, frameData.focal.y), new Vec2F(frameData.center.x, frameData.center.y), radialDisortion.ToList(), CameraModelType.OpenCV_Fisheye, CameraDeviceType.Back, 0);

    RGBCameraPose = frameData.cameraOffset.ToPose();
}

写头显扩展:图像帧

这里是你发送 Image frame dataEasyAR Sense 内部的地方。请参考 数据需求 来了解细节。

没有必要每帧调用。最小可接受帧率 = 2。它可以在任何线程调用,只要你的API都是线程安全的即可。这些数据需要与相机传感器曝光时的数据一致。只要可以获取,建议输入色彩数据到EasyAR Sense,这对EasyAR Mega的效果是有帮助的。为实现最佳效率,你可以设计整个数据链条让原始YUV数据直接通过共享内存透传,并直接使用数据指针传入EasyAR Sense。请注意数据所有权。

private void TryInputCameraFrameData()
{
    ...
    if (timestamp == curTimestamp) { return; } // NOTE: Do not waste time sending the same data again. And if possible, do not copy memory or do any time-consuming tasks in your own API getting camera data.
    curTimestamp = timestamp;

    ...
    // NOTE: Make sure dispose is called. There will be serious memory leak otherwise.
    using (buffer)
    using (var image = Image.create(buffer, format, size.x, size.y, pixelSize.x, pixelSize.y))
    using (var frame = InputFrame.create(image, cameraParameters, timestamp, cameraPose.ToEasyARPose(), trackingStatus))
    {
        sink.handle(frame);
        ReceivedFrameCount++;
    }

    throw new NotImplementedException("Please finish this method using your device SDK API");
}

重要 :不要忘记在使用后dispose EasyAR Sense的数据。否则会出现严重内存泄漏,buffer pool获取buffer也可能会失败。

举例, QiyuFrameSource 中的实现方式如下,

void Update()
{
    if (RGBCameraPose.OnNone) { return; }
    var frameData = new QiyuARPlugin.VstFrameDataNative();
    QiyuARPlugin.QVR_GetVstFrame(ref frameData);

    ...
    OnCameraFrameReceived(frameData);
}

private void OnCameraFrameReceived(QiyuARPlugin.VstFrameDataNative frameData)
{
    if (frameData.cameraTimeStamp == curTimestamp) { return; }

    curTimestamp = frameData.cameraTimeStamp;
    var size = new Vector2Int((int)frameData.size.x, (int)frameData.size.y);
    var pixelSize = size;
    var yLen = pixelSize.x * pixelSize.y;
    var bufferBlockSize = yLen;

    var bufferO = TryAcquireBuffer(bufferBlockSize);
    if (bufferO.OnNone) { return; }

    var buffer = bufferO.Value;
    buffer.tryCopyFrom(frameData.data, 0, 0, bufferBlockSize);

    var pose = RGBCameraPose.Value.GetTransformedBy(frameData.cameraPose.ToPose());
    // NOTE: Qiyu did not give a reasonable tracking status. Do not do this in your implementation.
    var trackingStatus = MotionTrackingStatus.Tracking;

    using (buffer)
    using (var image = Image.create(buffer, PixelFormat.Gray, size.x, size.y, pixelSize.x, pixelSize.y))
    using (var frame = InputFrame.create(image, cameraParameters, frameData.cameraTimeStamp * 1e-9, pose.ToEasyARPose(), trackingStatus))
    {
        sink.handle(frame);
        ReceivedFrameCount++;
    }
}

写头显扩展:渲染帧

这里是你发送 Rendering frame dataEasyAR Sense 内部的地方。请参考 数据需求 来了解细节。

请确保在设备数据准备好之后每帧调用,不能跳帧。这些数据需要与驱动同一帧内当前Unity渲染相机的数据一致。

private void InputRenderFrameMotionData()
{
    ...
    UpdateMotion(timestamp, cameraPose, trackingStatus);

    throw new NotImplementedException("Please finish this method using your device SDK API");
}

举例, QiyuFrameSource 中的实现方式如下,

void Update()
{
    if (RGBCameraPose.OnNone) { return; }
    var frameData = new QiyuARPlugin.VstFrameDataNative();
    QiyuARPlugin.QVR_GetVstFrame(ref frameData);

    if (frameData.headTimeStamp <= 0) { return; }
    // NOTE: Qiyu did not give a reasonable tracking status. Do not do this in your implementation.
    var trackingStatus = MotionTrackingStatus.Tracking;
    UpdateMotion(frameData.headTimeStamp * 1e-9, RGBCameraPose.Value.GetTransformedBy(frameData.headPose.ToPose()), trackingStatus);
    ...
}

运行验证(bring-up)头显扩展

准备用于运行验证的样例

样例位于 Samples~/SimpleDemo。为了在原地开发样例,请确保将文件夹名从 Samples~ 重命名为 Samples 。简单起见,sample中没有代码(除了导入StreamingAssets文件的脚本),全部由场景中配置实现。

  1. 添加你的设备支持物体到场景中。

你也可以反过来做,使用一个可以在你设备上运行的场景,然后添加EasyAR组件和sample场景中的其它物体到你的场景中。

  1. 如果场景中定义了任何原点,移动 "Cube" 和 "UI" 到原点节点下。

../_images/image_h2_3.png

"Cube"方块会提供一个设备SLAM行为的参照,这会帮助判断跟踪不稳定时候的原因。

  1. 设置"UI"的constraint source为你的渲染相机,以确保 "HUD" 按钮可以按预期工作。

../_images/image_h2_4.png
  1. 注意"UI" 节点下的"Canvas",确保raycast可以工作,以确保所有按钮和开关可以按预期工作。

../_images/image_h2_5.png
  1. 如果你当前没有使用 EasyAR Mega ,确保关闭 ARSession 下面的 Mega Tracker 物体,否则会出现错误。

../_images/image_h2_17.png

如果要了解sample场景中的细节以及正常sample是如何创建的,请参考 完成包:样例

构建和运行样例

请确保通过 样例使用说明 学习如何使用样例。对于Android的一些特殊配置,请阅读 Android 工程配置

你可以参考 EasyAR头显扩展样例说明 来了解如何使用样例,但有一个特殊情况需要了解, EasyAR 运动融合 在模板的样例中是关闭的,这将帮助更好地验证发送到EasyAR的数据的正确性。

为了运行图像跟踪,你需要打印namecard.jpg到A4纸,并确保图像宽度与纸的长边一致。

第一次在你的设备上运行验证EasyAR时,请确保先顺序运行这些功能,尤其是不要急于运行Mega,因为EasyAR Mega有一些容错性,在短时间运行或单一现实场景中运行的时候难以发觉。

  • 阅读眼前显示的 session dump信息,确保没有意外情况发送,并确保frame count在持续增长。

  • 运行 Image ,即 EasyAR 平面图像跟踪 功能,与手机运行效果对比。

  • 运行 Dense ,即 EasyAR 稠密空间地图 功能,与手机运行效果对比。

重要注意事项 :头显的支持是通过EasyAR Sense的自定义相机实现的。使用个人版的EasyAR Sense license或使用试用版本的Mega服务时,EasyAR每次启动将只能使用100秒。使用付费版本的EasyAR Sense和付费的EasyAR Mega服务没有这个限制。默认情况下,Unity插件会在100s后主动崩溃,你可以通过 弹出消息处理及错误围栏Fatal Response 选项来改变这个 "主动崩溃" 的行为。

运行验证过程中的问题分解

为使EasyAR在你的设备上工作,最重要的工作同时也是最棘手的部分是确保数据正确性。在一个新设备上首次运行验证EasyAR时,超过90%的问题都是由错误的数据造成的。强烈建议在没有EasyAR存在的时候,仅通过你的设备使用一些方法来直接验证数据的正确性。我们下面会提供一些使用EasyAR功能来验证你的数据的经验方法,它能帮助你理解 数据需求 ,但使用如此耦合的系统绝不是确保数据正确性的最佳方法。

如果 Image (跟踪和目标覆盖显示)以及 Dense (mesh位置、生成速度和质量)都和手机上的效果表现一致或更好(最好使用iPhone做对比)那么大部分EasyAR的功能都可以在你的设备上正常工作,你可以开始测试Mega(如果这是你的目标的话)。需要注意 EasyAR 稠密空间地图 可能无法在部分Android设备上运行,mesh质量也会根据设备而变化。

如果你无法重现与手机上相同的结果,那接下来是一个详细的问题分解过程,你可以参考它来寻找根因。

请确保始终关注adb的日志输出。 Dense spatial map存在已知问题,会在运行一段时间之后持续输出mesh组件相关的错误日志。这不会影响已经显示出来的mesh的质量,会在今后的版本中修复。

  1. 你的系统误差

    还记得准备阶段所描述的的SLAM和显示需求吗?

    SLAM/VIO误差会始终以不同方式影响EasyAR算法的稳定性。请始终记住这一点。

    显示系统误差可能会导致虚拟物体和现实物体无法完美对齐。在一些误差比较大的情形下,虚拟物体会看起来悬浮于真实物体上面或下面,然后(看起来)一直在漂移。这个现象可以在Pico 4E上观察到,即使不使用EasyAR只打开它自己的VST也有同样的现象。

  2. 会话状态显示及转储 显示

    必需的健康功能或数据:

    • Frame source Availability

    • Frame source Rendering Camera

    如果你看不到dump信息的显示,请尝试修改选项为 Log 然后阅读session的状态和正在使用的frame source的名字。

    可以尝试在 ARSession 中输出日志以找到系统中发生了什么。另外你还可以尝试删除在场景中的 ARSession 节点下所有其它frame source,然后看是否有什么变化。

  3. 接收到的Image Frame Count

    必需的健康功能或数据:

    • EasyAR Sense Unity Plugin 中 Frame source Image frame data 数据通路(不包含数据正确性以及到 EasyAR Sense 的数据通路)

    这个数据应该随时间增长,否则会在几秒之后弹出显示警告信息。

    如果你发现这个数值不增长,你应该debug以寻找原因。在模板代码中,这个数值是在TryInputCameraFrameData中设置的。

  4. 在设备上录制EIF,然后在Unity编辑器中回放

    必需的健康功能或数据:

    • Frame source Image frame dataEasyAR Sense 的数据通路(不包含数据正确性)

    点击 EIF 来启动录制,再次点击停止。你必须停止录制才能获取到可以使用的EIF文件,否则录制的文件将不可使用。请参考 FrameRecording 样例来使用EIF数据。在Unity编辑器中运行EIF数据时最好使用纯净的EasyAR场景或使用EasyAR的样例以避免场景中不正确的配置。

    使用EIF你可以做很多事情,你可以在Unity编辑器中使用EIF运行 EasyAR 平面图像跟踪EasyAR 稠密空间地图 。但是需要记住的是,在设备上运行的显示效果有可能是不一样的。

    EasyAR会在计算中使用畸变参数但不会对图像做反畸变。所以如果你输入了这些数据,当你在Unity中回放EIF文件时,你会观察到没有反畸变的数据,这是符合预期的。

    必需的健康功能或数据:

    • Image frame data 中的 Raw camera image data

    • Image frame data 中的 Timestamp (不包括时间点和数据同步)

    你可以在Unity编辑器中看到图像帧的回放。图像数据并不是字节相等的,整个流程中有有损编解码。修改Unity game窗口的比例与输入相同,否则数据会被裁剪显示。

    如果数据播放偏快或偏慢,你需要检查你的 Timestamp 输入。

  5. 使用EIF运行 EasyAR 平面图像跟踪

    必需的健康功能或数据:

    • Image frame data 中的 Raw camera image data

    • Image frame data 中的 Intrinsics (数据正确性不能完全保证,因为算法对误差存在容忍度)

    在Unity编辑器中使用EIF运行 ImageTracking_Targets 样例,你需要录制一个图像可以被跟踪到的EIF。

    请注意, EasyAR 平面图像跟踪 需要跟踪目标占据整个图像的一定比例,如果你无法跟踪到图像,尝试移动头到更加接近图像的位置。

    如果跟踪持续失败或虚拟物体显示在图像中远离目标的位置,则很有可能 Intrinsics 是错的。

    如果你的图像数据有畸变,你可能会看到虚拟物体不会完美的覆盖图像上的跟踪目标,这是符合预期的。

  6. 在设备上运行 EasyAR 平面图像跟踪

    必需的健康功能或数据:

    • 你的显示系统

    • Image frame data 中的 Raw camera image data

    • Image frame data 中的 Intrinsics (数据正确性不能完全保证,因为算法对误差存在容忍度)

    • Image frame dataRendering frame dataCamera pose 的坐标一致性

    • Image frame dataRendering frame dataCamera pose 的时间差

    请注意, EasyAR 平面图像跟踪 需要跟踪目标占据整个图像的一定比例,如果你无法跟踪到图像,尝试移动头到更加接近图像的位置。

    请注意, EasyAR 平面图像跟踪 需要目标的scale与真实世界中物体的大小一致,在样例中需要你跟踪一个宽度上撑满A4纸的图像,因此不要跟踪显示在电脑屏幕上的图像,除非你使用一把尺子并参照尺子将图像调整到A4大小。

    如果在使用EIF时图像跟踪很完美但在设备上却不同,请在继续其它测试前解决它。在后续步骤中解决问题要困难得多。

    如果虚拟物体悬浮显示在某个远离真实物体的地方,而且即使人不动也是如此,那很有可能 Intrinsics 不正确或 Image frame dataRendering frame dataCamera pose 不在同一个坐标系,或者你的显示系统产生了这个误差。

    如果虚拟物体在你移动头部的时候持续移动并且看起来就像是有延迟一样,那有很大可能性 Camera pose 不够像要求中描述的那样健康。这经常发生于几种情况,比如 Camera poseRaw camera image data 的数据时间不同步,或者 Image frame dataRendering frame data 中使用了相同的pose等等。

  7. 使用EIF或在设备上运行 EasyAR 稠密空间地图

    必需的健康功能或数据:

    • 你的显示系统

    • Image frame data 中的 Raw camera image data

    • Image frame data 中的 Intrinsics (数据正确性不能完全保证,因为算法对误差存在容忍度)

    • Image frame data 中的 Camera pose

    如果mesh生成速度非常慢和/或地面重建坑坑洼洼,那非常有可能 Camera pose 有问题。也有可能pose的坐标系不正确或pose的时间点不对。

    通常分辨精确的mesh位置不是非常容易,所以在使用 EasyAR 稠密空间地图 时你的显示系统误差可能不一定能观察出来。

EasyAR Mega

准备工作

在使用EasyAR Mega之前你需要首先阅读 EasyAR Mega 入门指南 。然后跟随 EasyAR Mega Unity 开发手册 学习使用EasyAR Mega时应该如何做Unity侧的开发。

请确保先在手机上运行Mega并了解流程。

如果你之前关闭了ARSession下的 Mega Tracker 物体,不要忘记启用它。

../_images/image_h2_18.png

运行验证过程中的问题分解

必需的健康功能或数据:

  • 你的显示系统

  • image frame datarendering frame data 中的所有数据

如果你跟随这篇文章成功运行验证了 EasyAR 平面图像跟踪 以及 EasyAR 稠密空间地图 两个功能,那么EasyAR Mega应该已经被支持了。如果在头显上运行的表现明显比手机上差,请重点关注 image frame datarendering frame data 中的pose数据和timestamp。另外还需要关注你的SLAM/VIO系统输出。 Origin 下面的方块会是一个好的参考。

如果你还没有尝试过 EasyAR 平面图像跟踪EasyAR 稠密空间地图 就在测试EasyAR Mega,那请返回上一段落,耐心地跟着前面的问题分解指引一条一条查看。

打包和发布

完成包:编辑器

修改MenuItems类中的 "HMD Template" 字符串以代表你的设备。如果你需要其它自定义编辑器功能,也可以添加其它脚本。

完成包:样例

样例位于 Samples~/SimpleDemo。为了在原地开发样例,请确保将文件夹名从 Samples~ 重命名为 Samples 。Unity的 Client.Pack 方法会在你打包一个新的发布时将其自动重命名为 Samples~

模板中的样例主要为了两个目的而提供,在运行验证过程中验证设备以及为下游用户提供使用参考。因此在发布给应用开发者之前需要先完成和完善这个样例。

首先,让我们看一下在我们发布整个模板之前是如何创建这个样例的。

  1. 创建 EasyAR ARSession

你可以参考 从零创建可运行的工程 来在场景中创建EasyAR组件。

创建 ARSession,

../_images/image_h2_6.png

往session中添加一些filter。下图显示了如何往session中添加image tracker。

../_images/image_h2_7.png
  1. 创建sample中需要使用的image target和sparse spatial map

举例来讲,使用这个菜单来创建image target,

../_images/image_h2_8.png

配置image target,

../_images/image_h2_9.png

请注意,在完成上述配置之后,Unity Scene窗口中显示的图像是gizmo。这个sample中通过一个quad来显示同一图像的虚拟物体。

添加显示在target上面的虚拟物体,

../_images/image_h2_10.png
  1. 添加一个立方体作为SLAM的参考

这个立方体对你们、我们以及下游用户都很重要,它用于解耦设备SLAM和EasyAR算法。

../_images/image_h2_11.png
  1. 添加功能选择的UI

../_images/image_h2_12.png
  1. 关闭启动中启用的EasyAR功能,并通过UI开关来打开它们

例如,图像跟踪的功能在启动时可以关闭,只需要设置对应组件的enable为false即可,

../_images/image_h2_13.png

然后添加UI开关处理,

../_images/image_h2_14.png

额外地,修改frame recorder的配置以便在运行时自动创建文件名,

../_images/image_h2_15.png

你需要使用你的设备SDK完成这个sample。你已经在 运行验证(bring-up)头显扩展 这个段落中做过,因此我们在这里略过详细步骤。请确保在场景中添加你的设备支持,然后关注 "Cube"、"UI" 以及 "UI" 下的 "Canvas" 节点的相关配置。

如果之前关闭过,不要忘记打开ARSession下的 Mega Tracker 物体,

../_images/image_h2_18.png

最后,在发布前打开图像跟踪的 EasyAR 运动融合 ,当然如果你希望用户以关闭运动跟踪的方式使用,你也可以不做修改.

../_images/image_h2_16.png

别忘了清理场景中的一些字符串,比如“(Move into Origin if there is any, set constraint source to your rendering camera)”或“Cube (Move into Origin if there is any)”,这些字符串是写给你而非app开发者的。

完成包:包定义

包本身的定义在package.json中,你可以根据 Unity创建自定义package的指南 来修改这个文件或创建一个新的包。请确保修改 package的 "name" 和 "displayName",否则它会与模板本身或其它供应商的扩展发生冲突。

完成包:重新生成meta

请重新生成package中所有文件的 .meta 文件。否则它们会与模板本身或其它供应商的扩展发生冲突。

发布

你可能还希望修改package中的其它一些文件,请确保在发布前仔细审查整个package。

检查扩展与你的设备SDK以及EasyAR Sense Unity Plugin的版本兼容性。请注意EasyAR 不使用Unity所要求的 semantic versioning。主要区别是,minor版本号的变化也可能会引入不兼容的变化,虽然并不总是如此。如果你在使用Mega预发布版本,每次更新都可能会有API修改。

建议使用Unity package来打包你的文件。但是如果你的设备SDK并没有准备好以package形式发布,你也可以选择通过asset package来发布。

你需要提醒你的用户,EasyAR license key的所有限制(尤其是针对自定义相机的限制)都适用于你发布的包。

EasyAR Sense 写头显扩展

你需要有一个用于替代Unity的3D引擎。这并没有很多人想的那么容易,所以正常来说你需要使用Unity并按上面几段文档的描述来操作。如果你在尝试在这样的平台上运行验证 EasyAR Sense ,那下面列出了一些你需要重点关注的事情。

我们在与一些厂商紧密合作,比如微信上的XR-Frame,支付宝上的Paladin,以及粒界。如果你在尝试让你的设备工作在这些平台上,请先与我们联系了解最新进展。

额外的背景知识和准备工作

  1. 精通你在使用的3D引擎。

  2. 知道如何处理c++/java/jni开发,以及(根据你使用的3D引擎提供的脚本语言)其它跨语言开发。

  3. EasyAR Sense Unity Plugin 看作 EasyAR Sense 的一个样例,你需要通过 EasyAR Sense Unity Plugin 的源码学习很多细节。

推荐步骤

  1. 运行验证(bring up) EasyAR Sense Unity Plugin + HMD:强烈建议先完成上面的 Unity端测试。 EasyAR Sense Unity Plugin 做了很多相关工作,这会利于整体的验证。

  2. 运行验证(bring up) EasyAR Sense + 3D engine:参考 EasyAR Sense Unity Plugin 来在你的3D引擎中和手机上运行验证 EasyAR Sense 。确保验证 EasyAR 平面图像跟踪 功能(如果可能,也要验证 EasyAR 稠密空间地图 功能)。

  3. 运行验证(bring up) EasyAR Sense + 3D engine + HMD:参考 EasyAR Sense Unity Plugin 和头显模板来你的3D引擎中和头显设备上运行验证 EasyAR Sense

关键点和与手机的差异

  • EasyAR Sense 初始化

  • EasyAR Mega的特殊代码路径

    • 重点关注 MegaTracker.setResultAsyncModeMegaTracker.getSyncResult 。确保在创建后调用 setResultAsyncMode(false) ,然后每个渲染帧使用 getSyncResult 替换 OutputFrame 的对应输出。你仍需要使用整个 OutputFrame 路径来完成其它任务,因此不要删除它。请参考 MegaTrackerFrameFilter.OnResultIsSyncResultRequired 部分的代码。

    • 不要连接Accelerometer的输出到MegaTracker.accelerometerResultSink ( EasyAR Sense Unity Plugin 中的 IsHMD 控制选项)。

    • 如果CLS内有多个block,使用一个父节点组织它们,并通过父节点进行整体的节点移动。

  • EasyAR 平面图像跟踪 和其它大多数功能的特殊代码路径

    • 根据OutputFrame中peek的数据(它是image frame data)和rendering frame data的运动数据差来变换pose。请参考 ImageTrackerFrameFilter.OnResultmotionWorkaround 部分的源码。

  • 3D相机 ( EasyAR Sense Unity Plugin 中的 IsCameraUnderControl 控制选项)

    • 不要使用EasyAR Sense的数据渲染相机图像。

    • 不要使用EasyAR Sense的数据设置3D相机的投影矩阵。

    • 不要使用EasyAR Sense的数据设置相机的transform。

  • 中心模式设计,原点设计和节点/相机控制

    • 如果可能,设计类似 ARSession.ARCenterMode 的中心模式。

    • 强烈建议设计类似 XR.CoreUtils.XROrigin 的原点。

    • 最佳方式是做类似Unity XR框架的 XR.CoreUtils.XROrigin 的设计,使用设备数据设置3D相机的local transform,然后通过 EasyAR Sense 中返回的数据移动 origin。如果在整体设计中没有 Origin ,则需要使用设备数据设置3D相机的world transform,然后通过 EasyAR Sense 中返回的数据移动 EasyAR Sense 的 target 或 block (这就是 EasyAR Sense Unity Plugin 中的 SessionOrigin 中心模式)。

    • 永远不要通过 EasyAR Sense 中计算的target pose来设置3D相机的 transform,如果这么做显示将是错的。

    • 你可以参考 EasyAR Sense Unity Plugin 的源代码来做 pose 计算,可以从 ARSession.OnFrameUpdate 开始理解代码。

额外的问题分解方法

  • EasyAR Sense Unity Plugin 对比并寻找差异

  • 对Mega来说,可以对比 Mega Toolbox

  • Origin 下放置方块或其它物体来解耦设备SLAM和EasyAR算法

  • 录制EIF然后在Unity编辑器(或你的3D编辑器)中播放

  • 录制rendering frame data用来分析(rendering frame data没有记录在EIF文件中)

  • 使用类似 运行验证(bring-up)头显扩展 中描述的问题分解步骤